package com.inspinia.base.util;

import com.inspinia.base.common.model.BaseConstants;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 参考：http://www.cnblogs.com/tiantianbyconan/archive/2013/02/20/2919132.html
 *
 * @author wangjie
 * @version 创建时间：2013-2-7 下午1:40:52
 */
public class FileDownloader {

    //休眠时间
    private static final long SLEEP_SECONDS = 500L;
    /**
     * 日志对象
     */
    protected Logger logger = LoggerFactory.getLogger(FileDownloader.class);

    //一组开始下载位置
    private long[] startPos;
    //一组结束下载位置
    private long[] endPos;
    private String url;
    private String destFilePath;
    private FileDownloadThread[] fileItems;
    //临时文件
    private File tempFile;
    //http的超时时间
    private FileDownloadListener fileDownloadListener; // 进度监听器
    private FileDownloaderNotificationThread notificationThread; // 通知进度线程
    //存储文件位置的文件
    private File positionFile;
    //本地文件
    private File destFile;
    //分成多少段下载， 每一段用一个线程完成下载
    private int splitter;
    //位置文件的后缀
    private static final String POSITION_FILE_SUFFIX = ".position";
    /**
     * 文件下载方式。
     */
    private DownloadMode downloadMode;
    /**
     * 下载过程中文件的后缀名。
     */
    public final static String TEMP_FILE_SUFFIX = ".temp";
    /**
     * 默认使用的线程数量。<br>
     * 如果不设置线程数量参数（splitter），则默认线程启动数量为1，即单线程下载。
     */
    public static final int DEFAULT_splitter = 1;

    //本次下载的文件总大小
    private long downloadSize;
    private AtomicInteger completedThread = new AtomicInteger(0);
    //true表示下载没有被取消
    private boolean isRunning = true;

    //读取数据超时时间
    public int readTimeout = (int) (BaseConstants.ONE_MINUTE_MS);
    //http连接超时时间
    public int connectTimeout = (int) (BaseConstants.ONE_MINUTE_MS);
    //true表示读取超时后重新下载。false则终止下载。
    public boolean reDownloadAfterGotReadTimeout = true;
    //远程文件的大小
    private long remoteFileSize;

    /**
     * 生成JFileDownloader对象。
     *
     * @param url          要下载的目标文件URL路径
     * @param destFilePath 要保存的文件目标（路径+文件名）
     * @param splitter     下载该文件所需要的线程数量
     */
    public FileDownloader(String url, String destFilePath, int splitter) {
        this.url = url;
        this.destFilePath = destFilePath;
        this.splitter = splitter;
    }

    /**
     * 生成JFileDownloader对象，其中下载线程数量默认是1，也就是选择单线程下载。
     *
     * @param url          url 要下载的目标文件URL路径
     * @param destFilePath destFilePath 要保存的文件目标（路径+文件名）
     */
    public FileDownloader(String url, String destFilePath) {
        this(url, destFilePath, DEFAULT_splitter);
    }

    /**
     * 默认的构造方法，使用构造方法后必须要调用set方法来设置url等下载所需配置。
     */
    public FileDownloader() {

    }

    /**
     * 指定日期下载的线程池
     */
    private static ThreadPoolExecutor threadExecutor = new  ThreadPoolExecutor(128, 2048, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(2048), new ThreadPoolExecutor.CallerRunsPolicy());

    /**
     * 将写入点数据保存在临时文件中
     *
     * @throws IOException
     * @author hoojo
     * @createDate 2011-9-23 下午05:25:37
     */
    private void writePosition(DataOutputStream dos) throws IOException {
        boolean close = false;
        if (dos == null) {
            close = true;
            dos = new DataOutputStream(new FileOutputStream(positionFile));
        }
        try {
            dos.writeInt(startPos.length);
            for (int i = 0; i < startPos.length; i++) {
                dos.writeLong(fileItems[i].getStartPos());
                dos.writeLong(fileItems[i].getEndPos());
            }
        } finally {
            if (close) {
                dos.flush();
                IOUtils.closeQuietly(dos);
            }
        }
    }

    /**
     * <b>function:</b>读取写入点的位置信息
     *
     * @throws IOException
     * @author hoojo
     * @createDate 2011-9-23 下午05:30:29
     */
    private void readPosition() throws IOException {
        DataInputStream dis = new DataInputStream(new FileInputStream(positionFile));
        try {
            int startPosLength = dis.readInt();
            startPos = new long[startPosLength];
            endPos = new long[startPosLength];
            for (int i = 0; i < startPosLength; i++) {
                startPos[i] = dis.readLong();
                endPos[i] = dis.readLong();
            }
        } finally {
            IOUtils.closeQuietly(dis);
        }
    }


    /**
     * 开始下载方法（流程分为3步）。
     * <br><ul>
     * <li>检验URL的合法性<br>
     * <li>计算下载所需的线程数量和每个线程需下载多少大小的文件<br>
     * <li>启动各线程。
     * </ul>
     *
     * @throws Exception 如果设置的URL，includes等参数不合法，则抛出该异常
     * @author wangjie
     */
    public long startDownload() throws IOException, HttpStateCodeNot200Or206Exception {
        // 检验参数合法性
        checkSettingfValidity();

        //在这里初始化原因是path一定是文件路径
        this.destFile = new File(destFilePath);

        // 获取文件长度，如果文件长度不合法，则不用下载。
        remoteFileSize = calcRemoteFileSize();
        if (remoteFileSize <= 0) {
            return remoteFileSize;
        }

        //计算分区
        calcPosition(remoteFileSize);

        splitter = startPos.length;

        // 本次下载的数据量
        downloadSize = endPos[endPos.length - 1] - startPos[0];

        createTempFile();

        // 初始化若干个下载线程
        initFileItems();

        //等下各个线程下载完成
        waitDownloading();

        return remoteFileSize;
    }

    /**
     * 创建临时文件
     *
     * @throws IOException
     */
    private void createTempFile() throws IOException {
        tempFile = new File(destFilePath + TEMP_FILE_SUFFIX);
        switch (downloadMode) {
            case override:
                reCreateFile(tempFile);
                break;
            case continued:
                if (!tempFile.exists()) {
                    tempFile.getParentFile().mkdirs();
                    tempFile.createNewFile();
                }
                break;
        }
    }

    /**
     * 启动通知线程
     */
    private void startNotificationThread() {
        notificationThread = new FileDownloaderNotificationThread(fileItems, fileDownloadListener, destFile, downloadSize, tempFile, positionFile);
        threadExecutor.execute(notificationThread);
    }

    /**
     * 初始化每个线程
     */
    private void initFileItems() throws IOException {
        fileItems = new FileDownloadThread[splitter];

        // 启动进度通知线程
        startNotificationThread();

        for (int i = 0; i < splitter; i++) {
            fileItems[i] = new FileDownloadThread(this, this.url, tempFile,
                    startPos[i], endPos[i], notificationThread, completedThread, readTimeout, connectTimeout, reDownloadAfterGotReadTimeout);
            fileItems[i].setPriority(8);
        }
        // 启动若干个下载线程（因为下载线程JFileDownloaderNotificationThread中使用了threads属性，所以必须等下载线程全部初始化以后才能启动线程）
        for (FileDownloadThread thread : fileItems) {
            threadExecutor.execute(thread);
        }
    }

    /**
     * 等待线程执行下载操作
     */
    private void waitDownloading() throws IOException {
        //循环写入下载文件长度信息
        DataOutputStream dos = null;
        try {
            dos = new DataOutputStream(new FileOutputStream(positionFile));
            while (completedThread.intValue() < splitter && isRunning) {
                try {
                    if (logger.isDebugEnabled()) {
                        logger.debug("正在写入文件位置信息");
                    }
                    writePosition(dos);
                    Thread.sleep(SLEEP_SECONDS);
                } catch (InterruptedException e) {
                    logger.error("", e);
                }
            }
            //如果下载没有被终止，就让通知线程发起下载完成通知。
            if (isRunning && notificationThread.isRunning()) {
                notificationThread.downloadCompleted();
            }

        } finally {
            IOUtils.closeQuietly(dos);
        }
    }


    /**
     * 重新创建文件
     *
     * @param file
     * @throws IOException
     */
    private void reCreateFile(File file) throws IOException {
        file.delete();
        file.getParentFile().mkdirs();
        file.createNewFile();
    }

    /**
     * 计算每个线程下载量。
     *
     * @param remoteFileSize 下载的目标文件的大小
     */
    private void calcPosition(long remoteFileSize) throws IOException {
        String positionFilePath = this.destFilePath + POSITION_FILE_SUFFIX;
        positionFile = new File(positionFilePath);

        //数组的长度就要分成多少段的数量
        long start = 0;

        //如果是续传，没有位置信息文件的时候，就从本地文件本身的大小开始下载，如果有，则使用位置文件信息。
        switch (downloadMode) {
            case override:
                break;
            case continued:

                //如果存在读入点位置的文件
                if (positionFile.exists()) {
                    //注意：这里只根据文件大小来判断文件内容是否改变，如果该判断不适用，请直接用override方式下载。
                    readPosition();
                    if (splitter > (startPos.length * 2)) {
                        //如果新的分区数量大于老的分区数量2倍，则放弃之前已经下载的数据。因为此时很难计算要从哪里开始。
                        //续传会从本地文件大小处开始。
                        start = destFile.length();
                    } else {
                        //如果上次断点的时候和本次下载的时候文件大小不同，则需要重新调整。
                        long lastRemoteFileSize = endPos[endPos.length - 1];
                        if (lastRemoteFileSize < remoteFileSize) {
                            //再加一个分区进来。
                            long[][] longs = PhoneNumberUtil.splitLong(lastRemoteFileSize, remoteFileSize, splitter);
                            startPos = ArrayUtils.addAll(startPos, longs[0]);
                            endPos = ArrayUtils.addAll(endPos, longs[1]);
                            return;
                        } else if (lastRemoteFileSize == remoteFileSize) {
                            return;
                        }
                    }
                }
                break;
        }
        long[][] longs = PhoneNumberUtil.splitLong(start, remoteFileSize, splitter);
        startPos = longs[0];
        endPos = longs[1];
    }


    /**
     * <b>function:</b> 获取下载文件的长度
     *
     * @return
     * @author hoojo
     * @createDate 2011-9-26 下午12:15:08
     */
    public long calcRemoteFileSize() throws IOException, HttpStateCodeNot200Or206Exception {
        long size = HttpClientUtil.getRemoteFileLength(this.url);
        //如果远程文件大小为0，则直接创建文件，跳过分段下载。
        if (size == 0) {
            if (!destFile.exists()) {
                Path parentPath = Paths.get(destFile.getParent());
                if (!Files.exists(parentPath)) {
                    Files.createDirectory(parentPath);
                }
                Files.createFile(destFile.toPath());
            }
            return 0;
        }
        //续传模式下，本地文件大小不为0，且和远程文件大小相等的时候，不用下载。当文件大小为0的时候，依然要下载。
        if (DownloadMode.continued == downloadMode && size == destFile.length() && destFile.length() != 0) {
            return 0;
        }
        return size;
    }

    public long getRemoteFileSize() {
        return remoteFileSize;
    }

    /**
     * 删除下载过程中产生的临时文件。
     *
     * @throws IOException
     */
    public void deleteTempFiles() throws IOException {
        Files.delete(tempFile.toPath());
        Files.delete(positionFile.toPath());
    }


    /**
     * 取消所有下载线程。
     *
     * @author wangjie
     */
    public void cancelDownload() {
        isRunning = false;
        if (null != fileItems && 0 != fileItems.length && null != notificationThread) {
            for (FileDownloadThread thread : fileItems) { // 终止所有下载线程
                thread.cancelThread();
            }
            notificationThread.cancelThread(); // 终止通知线程
            logger.info("下载已被终止:" + url);
            return;
        }
    }

    /**
     * 设置要下载的目标文件URL路径。
     *
     * @param url 要下载的目标文件URL路径
     * @return 返回当前JFileDownloader对象
     * @author wangjie
     */
    public FileDownloader setUrl(String url) {
        this.url = url;
        if (url.endsWith("/")) {
            throw new RuntimeException("url没有指向一个文件");
        }
        return this;
    }

    /**
     * 设置要保存的目标文件（路径+文件名）。
     *
     * @param destFilePath 要保存的文件目标（路径+文件名）
     * @return 返回当前JFileDownloader对象
     * @author wangjie
     */
    public FileDownloader setDestFilePath(String destFilePath) {
        this.destFilePath = destFilePath;
        //如果用户传的是路径，则使用url中的文件名。
        if (destFilePath.endsWith("/")) {
            this.destFilePath += FilenameUtils.getName(url);
        }
        return this;
    }

    /**
     * 设置下载该文件所需要的线程数量。
     *
     * @param splitter 下载该文件所需要的线程数量
     * @return 返回当前JFileDownloader对象
     * @author wangjie
     */
    public FileDownloader setSplitter(int splitter) {
        if (splitter < 0) {
            splitter = 1;
        }
        this.splitter = splitter;
        return this;
    }


    /**
     * 设置监听器，以获取下载进度。
     */
    public FileDownloader setFileDownloadListener(
            FileDownloadListener fileDownloadListener) {
        this.fileDownloadListener = fileDownloadListener;
        return this;
    }

    public FileDownloader setDownloadMode(DownloadMode downloadMode) {
        this.downloadMode = downloadMode;
        return this;
    }

    /**
     * 设置读取超时时间
     *
     * @param readTimeout
     * @return
     */
    public FileDownloader setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
        return this;
    }

    /**
     * 设置链接超时时间
     *
     * @param connectTimeout
     * @return
     */
    public FileDownloader setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
        return this;
    }

    /**
     * 设置读取超时后，是否继续下载。默认是true（重新下载）
     *
     * @param reDownloadAfterGotReadTimeout
     * @return
     */
    public FileDownloader setReDownloadAfterGotReadTimeout(boolean reDownloadAfterGotReadTimeout) {
        this.reDownloadAfterGotReadTimeout = reDownloadAfterGotReadTimeout;
        return this;
    }

    /**
     * 通过该方法移出相应的监听器对象。
     *
     * @param fileDownloadListener 要移除的监听器对象
     * @author wangjie
     */
    public void removeFileDownloadListener(
            FileDownloadListener fileDownloadListener) {
        fileDownloadListener = null;
    }

    /**
     * 获取下载的文件总大小。
     * 在调用开始下载方式后才能获取到
     *
     * @return
     */
    public long getDownloadSize() {
        return downloadSize;
    }

    /**
     * 检验设置的参数是否合法。
     *
     * @throws Exception 目标文件URL路径不合法，或者线程数小于1，则抛出该异常
     * @author wangjie
     */
    private void checkSettingfValidity() {
        if (null == url || "".equals(url)) {
            throw new RuntimeException("目标文件URL路径不能为空");
        }
        if (splitter < 1) {
            throw new RuntimeException("线程数不能小于1");
        }
    }


    // 以下代码暂时保留
    public static void main(String[] args) {
        String urlPath = "http://apache.mirrors.pair.com/lucene/solr/7.1.0/solr-7.1.0.tgz";
        String destFilePath = "F:/solr/";
        int splitter = 100;

//        FileDownloader downloader = new FileDownloader(url, destFilePath, splitter);
        //或者：
        FileDownloader downloader = new FileDownloader()
                .setUrl(urlPath)
                .setDestFilePath(destFilePath)
                .setSplitter(splitter)
                .setReadTimeout(30000)
                .setDownloadMode(DownloadMode.continued)
                .setFileDownloadListener(new FileDownloadListener() { // 设置进度监听器
                    @Override
                    public void downloadProgress(int progress, double speed, long remainTime) {
                        System.out.println("文件已下载：" + progress + "%，下载速度为：" + speed + "kb/s，剩余所需时间：" + DateUtil.formatLongToTimeStr(remainTime));
                    }

                    @Override
                    public void downloadCompleted(File file, long downloadTime) {
                        System.out.println("文件：" + file.getName() + "下载完成，用时：" + DateUtil.formatLongToTimeStr(downloadTime));
                    }

                    @Override
                    public void downloadError(Exception e) {
                        e.printStackTrace();
                    }
                });
        try {
            downloader.startDownload(); // 开始下载
            threadExecutor.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}